package org.mixed.service;

import io.micrometer.common.util.StringUtils;
import org.mixed.core.domain.ApiResponse;
import org.mixed.core.domain.Constants;
import org.mixed.core.domain.MixNotify;
import org.mixed.core.domain.TreeNode;
import org.mixed.core.enums.JsonCodeConstants;
import org.mixed.core.event.EventAction;
import org.mixed.core.event.FileEvent;
import org.mixed.core.event.GroupEvent;
import org.mixed.core.exception.InvalidArgumentException;
import org.mixed.core.resource.Info;
import org.mixed.core.resource.Resource;
import org.mixed.core.resource.ResourceStorage;
import org.mixed.core.resource.info.DataSourceInfo;
import org.mixed.core.resource.info.GroupInfo;
import org.mixed.core.utils.IoUtils;
import org.mixed.core.utils.JsonUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Supplier;
import java.util.stream.Collectors;

@Service
public class MixResourceService implements ApplicationListener<ApplicationStartedEvent>, JsonCodeConstants {
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    @Autowired
    private ApplicationEventPublisher publisher;
    private final Map<String, Resource> groupMappings = new HashMap<>(16);
    private final Map<String, GroupInfo> groupCache = new HashMap<>(16);
    private final Map<String, Resource> fileMappings = new HashMap<>(32);
    private final Map<String, Info> fileCache = new HashMap<>(32);
    private final Map<String, Map<String, String>> pathCache = new HashMap<>(16);
    private final Map<String, ResourceStorage<? extends Info>> storages;
    private final Resource resource;

    public MixResourceService(Resource resource, List<ResourceStorage<? extends Info>> storages) {
        this.resource = resource;
        this.storages = storages.stream()
                .peek(it -> it.setResourceService(this))
                .collect(Collectors.toMap(ResourceStorage::folder, it -> it));
    }

    public ResourceStorage<? extends Info> getStorage(String folder) {
        return storages.get(folder);
    }

    private <R> R readLock(Supplier<R> supplier) {
        try {
            lock.readLock().lock();
            return supplier.get();
        } finally {
            lock.readLock().unlock();
        }
    }

    private <R> R writeLock(Supplier<R> supplier) {
        try {
            lock.writeLock().lock();
            return supplier.get();
        } finally {
            lock.writeLock().unlock();
        }
    }

    private void init() {
        groupMappings.clear();
        groupCache.clear();
        fileMappings.clear();
        fileCache.clear();
        pathCache.clear();
        storages.forEach((key, storage) -> {
            if (storage.requirePath()) {
                pathCache.put(storage.folder(), new HashMap<>(32));
            }
            Resource folder = resource.getDirectory(key);
            if (storage.allowRoot()) {
                String rootId = key + ":0";
                GroupInfo group = new GroupInfo();
                group.setId(rootId);
                group.setType(key);
                group.setParentId("0");
                putGroup(group, folder);
            }
            if (!folder.exists()) {
                folder.mkdir();
            }
        });
    }

    private void putGroup(GroupInfo group, Resource resource) {
        groupMappings.put(group.getId(), resource);
        groupCache.put(group.getId(), group);
    }

    private void putFile(ResourceStorage<?> storage, Info info, Resource resource) {
        fileMappings.put(info.getId(), resource);
        fileCache.put(info.getId(), info);
        if (storage.requirePath()) {
            pathCache.get(storage.folder()).put(info.getId(), storage.buildKey(info));
        }
    }

    public Resource getGroupResource(String id) {
        return groupMappings.get(id);
    }

    private List<GroupInfo> findGroups(String groupId) {
        return readLock(() -> {
            List<GroupInfo> groups = new ArrayList<>();
            String key = groupId;
            while (groupCache.containsKey(key)) {
                GroupInfo group = groupCache.get(key);
                groups.add(0, group);
                key = group.getGroupId();
            }
            return groups;
        });
    }

    public String getGroupName(String groupId) {
        return findGroups(groupId).stream()
                .map(GroupInfo::getName)
                .collect(Collectors.joining("/"));
    }

    public String getGroupPath(String groupId) {
        return findGroups(groupId).stream()
                .map(GroupInfo::getPath)
                .filter(StringUtils::isNotBlank)
                .collect(Collectors.joining("/"));
    }

    private void refreshGroup(Resource folder, ResourceStorage<? extends Info> storage) {
        if (storage.allowRoot()) {
            folder.files(storage.suffix()).forEach(file -> putFile(storage, storage.readResource(file), file));
        } else {
            folder.dirs().forEach(dir -> {
                Resource meta = dir.getResource(Constants.GROUP_METABASE);
                if (meta.exists()) {
                    putGroup(JsonUtils.readValue(meta.read(), GroupInfo.class), dir);
                    dir.files(storage.suffix()).forEach(file -> putFile(storage, storage.readResource(file), file));
                }
            });
        }
    }

    private void read(boolean triggerEvent) {
        writeLock(() -> {
            if (triggerEvent) {
//                publisher.publishEvent(new MagicEvent("clear", EventAction.CLEAR));
            }
            this.readAll();
            fileCache.values().forEach(entity -> {
                GroupInfo group = groupCache.get(entity.getGroupId());
//                publisher.publishEvent(new FileEvent(group.getType(), EventAction.LOAD, entity));
            });
            return null;
        });
    }

    private void readAll() {
        writeLock(() -> {
            init();
            resource.readAll();
            storages.forEach((key, storage) -> refreshGroup(resource.getDirectory(key), storage));
            return null;
        });
    }

    public <T extends Info> T file(String id) {
        return (T) fileCache.get(id);
    }

    public boolean pathNotMatch(String folder, String path, String id) {
        return pathCache.get(folder).entrySet().stream()
                .noneMatch(entry -> entry.getValue().equals(path) && !entry.getKey().equals(id));
    }

    public boolean dsNotMatch(DataSourceInfo info) {
        return listFiles("datasource:0").stream()
                .map(it -> (DataSourceInfo) it)
                .filter(it -> !it.getId().equals(info.getId()))
                .noneMatch(it -> Objects.equals(it.getKey(), info.getKey()));
    }


    public <T extends Info> List<T> listFiles(String groupId) {
        return readLock(() -> {
            GroupInfo group = groupCache.get(groupId);
            return fileCache.values().stream()
                    .filter(it -> it.getGroupId().equals(groupId))
                    .map(it -> (T) it)
                    .collect(Collectors.toList());
        });
    }

    public <T extends Info> boolean saveFile(T info) {
        isTrue(!resource.readonly(), IS_READ_ONLY);
        return writeLock(() -> {
            EventAction action = info.getId() == null || !fileCache.containsKey(info.getId()) ? EventAction.CREATE : EventAction.SAVE;
            // 获取所在分组
            Resource groupResource = getGroupResource(info.getGroupId());
            // 分组需要存在
            notNull(groupResource, GROUP_NOT_FOUND);
            ResourceStorage<T> storage;
            if (info.getGroupId().contains(":")) {
                storage = (ResourceStorage<T>) this.storages.get(info.getGroupId().split(":")[0]);
            } else {
                // 读取分组信息
                GroupInfo group = groupCache.get(info.getGroupId());
                storage = (ResourceStorage<T>) this.storages.get(group.getType());
            }
            storage.validate(info);
            // 拼接文件名
            String filename = info.getName() + storage.suffix();
            // 获取修改前的信息
            Resource fileResource = groupResource.getResource(filename);
            if (action == EventAction.CREATE) {
                if (info.getId() == null) {
                    isTrue(!fileResource.exists(), FILE_SAVE_FAILURE);
                    // 新增操作赋值
                    info.setId(UUID.randomUUID().toString().replace("-", ""));
                }
                info.setCreateTime(System.currentTimeMillis());
//                info.setCreateBy(WebUtils.currentUserName());
            } else {
                // 修改操作赋值
                info.setUpdateTime(System.currentTimeMillis());
//                info.setUpdateBy(WebUtils.currentUserName());
                isTrue(!Constants.LOCK.equals(fileCache.get(info.getId()).getLock()), RESOURCE_LOCKED);
                Resource oldFileResource = fileMappings.get(info.getId());
                if (!oldFileResource.name().equals(fileResource.name())) {
                    // 重命名
                    isTrue(oldFileResource.renameTo(fileResource), FILE_SAVE_FAILURE);
                }
            }
            boolean flag = fileResource.write(storage.write(info));
            if (flag) {
                publisher.publishEvent(new FileEvent(storage.folder(), action, info));
                putFile(storage, info, fileResource);
            }
            return flag;
        });
    }

    public boolean saveGroup(GroupInfo group) {
        isTrue(!resource.readonly(), IS_READ_ONLY);
        // 类型校验
        isTrue(storages.containsKey(group.getType()), NOT_SUPPORTED_GROUP_TYPE);
        // 名字校验
        notNull(group.getName(), NAME_REQUIRED);
        notNull(IoUtils.validateFileName(group.getName()), NAME_INVALID);
        // 需要填写groupId
        notNull(group.getParentId(), GROUP_ID_REQUIRED);
        ResourceStorage<? extends Info> storage = storages.get(group.getType());
        return writeLock(() -> {
            Resource res;
            // 判断是否要保存到根节点下
            if (Constants.ROOT_ID.equals(group.getParentId())) {
                res = resource.getDirectory(group.getType());
            } else {
                // 找到上级分组
                res = getGroupResource(group.getParentId());
                // 上级分组需要存在
                isTrue(res != null && res.exists(), GROUP_NOT_FOUND);
            }
            Resource groupResource;
            GroupEvent event = new GroupEvent(group.getType(), group.getId() == null ? EventAction.CREATE : EventAction.SAVE, group);
            if (group.getId() == null || !groupCache.containsKey(group.getId())) {
                // 添加分组
                if (group.getId() == null) {
                    group.setId(UUID.randomUUID().toString().replace("-", ""));
                }
                group.setCreateTime(System.currentTimeMillis());
//                group.setCreateBy(WebUtils.currentUserName());
                groupResource = res.getDirectory(group.getName());
                // 判断分组文件夹需要不存在
                isTrue(!groupResource.exists(), FILE_SAVE_FAILURE);
                // 创建文件夹
                groupResource.mkdir();
            } else {
                GroupInfo oldGroup = groupCache.get(group.getId());
                if (storage.requirePath() && !Objects.equals(oldGroup.getPath(), group.getPath())) {
                    TreeNode<GroupInfo> treeNode = tree(group.getType());
                    String oldPath = oldGroup.getPath();
                    oldGroup.setPath(group.getPath());
                    // 递归找出该组下的文件
                    List<Info> entities = treeNode.findTreeNode(it -> it.getId().equals(group.getId()))
                            .flat()
                            .stream()
                            .flatMap(it -> fileCache.values().stream().filter(f -> f.getGroupId().equals(it.getId())))
                            .collect(Collectors.toList());
                    for (Info entity : entities) {
                        String newMappingKey = storage.buildKey(entity);
                        if (pathCache.get(group.getType()).entrySet().stream().anyMatch(entry -> entry.getValue().equals(newMappingKey) && !entry.getKey().equals(entity.getId()))) {
                            // 还原path
                            oldGroup.setPath(oldPath);
                            throw new InvalidArgumentException(SAVE_GROUP_PATH_CONFLICT);
                        }
                    }
                }
                Resource oldResource = getGroupResource(group.getId());
                groupResource = res.getDirectory(group.getName());
                isTrue(oldResource != null && oldResource.exists(), GROUP_NOT_FOUND);
                // 设置修改时间，修改人
//                group.setUpdateBy(WebUtils.currentUserName());
                group.setUpdateTime(System.currentTimeMillis());
                // 名字不一样时重命名
                if (!Objects.equals(oldGroup.getName(), group.getName())) {
                    // 判断分组文件夹需要不存在
                    isTrue(!groupResource.exists(), FILE_SAVE_FAILURE);
                    isTrue(oldResource.renameTo(groupResource), FILE_SAVE_FAILURE);
                }
            }
            // 写入分组信息
            if (groupResource.getResource(Constants.GROUP_METABASE).write(JsonUtils.toJsonString(group))) {
                putGroup(group, groupResource);
                TreeNode<GroupInfo> treeNode = tree(group.getType()).findTreeNode(it -> it.getId().equals(group.getId()));
                // 刷新分组缓存
                refreshGroup(groupResource, storage);
                if (event.getAction() != EventAction.CREATE) {
                    event.setEntities(treeNode
                            .flat()
                            .stream()
                            .flatMap(g -> listFiles(g.getId()).stream())
                            .collect(Collectors.toList()));
                }
                publisher.publishEvent(event);
                return true;
            }
            return false;
        });
    }

    private List<Info> deleteGroup(String id) {
        isTrue(!resource.readonly(), IS_READ_ONLY);
        GroupInfo group = groupCache.get(id);
        List<Info> entities = new ArrayList<>();
        // 递归删除分组和文件
        tree(group.getType())
                .findTreeNode(it -> it.getId().equals(id))
                .flat()
                .forEach(g -> {
                    groupCache.remove(g.getId());
                    groupMappings.remove(g.getId());
                    fileCache.values().stream()
                            .filter(f -> f.getGroupId().equals(g.getId())).peek(entities::add)
                            .collect(Collectors.toList())
                            .forEach(file -> {
                                fileCache.remove(file.getId());
                                fileMappings.remove(file.getId());
                                Map<String, String> map = pathCache.get(g.getType());
                                if (map != null) {
                                    map.remove(file.getId());
                                }
                            });
                });
        groupMappings.remove(id);
        groupCache.remove(id);
        return entities;
    }

    public Map<String, TreeNode<GroupInfo>> tree() {
        return readLock(() -> groupCache.values().stream()
                .collect(Collectors.groupingBy(GroupInfo::getType,
                        Collectors.collectingAndThen(Collectors.toList(), this::convertToTree))));
    }

    public TreeNode<GroupInfo> tree(String type) {
        return readLock(() -> groupCache.values().stream()
                .filter(it -> type.equals(it.getType()))
                .collect(Collectors.collectingAndThen(Collectors.toList(), this::convertToTree)));
    }


    private TreeNode<GroupInfo> convertToTree(List<GroupInfo> groups) {
        TreeNode<GroupInfo> root = new TreeNode<>();
        root.setNode(new GroupInfo("0", "root"));
        convertToTree(groups, root);
        return root;
    }

    private void convertToTree(List<GroupInfo> remains, TreeNode<GroupInfo> root) {
        GroupInfo info;
        List<TreeNode<GroupInfo>> childNodes = new LinkedList<>();
        Iterator<GroupInfo> iterator = remains.iterator();
        while (iterator.hasNext()) {
            info = iterator.next();
            if (root.getNode().getId().equals(info.getParentId())) {
                childNodes.add(new TreeNode<>(info));
                iterator.remove();
            }
        }
        root.setChildren(childNodes);
        childNodes.forEach(it -> convertToTree(remains, it));
    }

    public boolean processNotify(MixNotify notify) {
        if (Constants.EVENT_TYPE_FILE.equals(notify.getType())) {
            return processFileNotify(notify.getId(), notify.getAction());
        }
        if (notify.getAction() == EventAction.CLEAR) {
            this.read(false);
            return true;
        }
        return processGroupNotify(notify.getId(), notify.getAction());
    }

    private boolean processGroupNotify(String id, EventAction action) {
        GroupInfo group = groupCache.get(id);
        if (group == null) {
            // create
            this.readAll();
            group = groupCache.get(id);
        }
        TreeNode<GroupInfo> treeNode = tree(group.getType()).findTreeNode(it -> it.getId().equals(id));
        if (treeNode != null) {
            GroupEvent event = new GroupEvent(group.getType(), action, group);
            event.setSource(Constants.EVENT_SOURCE_NOTIFY);
            if (event.getAction() == EventAction.DELETE) {
                event.setEntities(deleteGroup(id));
            } else if (action != EventAction.CREATE) {
                Resource folder = groupMappings.get(id);
                folder.readAll();
                if (folder.exists()) {
                    // 刷新分组缓存
                    refreshGroup(folder, storages.get(group.getType()));
                } else {
                    this.readAll();
                    treeNode = tree(group.getType()).findTreeNode(it -> it.getId().equals(id));
                }
                event.setGroup(groupCache.get(id));
                event.setEntities(treeNode
                        .flat()
                        .stream()
                        .flatMap(g -> listFiles(g.getId()).stream())
                        .collect(Collectors.toList()));
            }
            publisher.publishEvent(event);
            return true;
        }
        return false;
    }

    private boolean processFileNotify(String id, EventAction action) {
        Info entity = fileCache.get(id);
        if (entity == null) {    // create
            this.readAll();
            entity = fileCache.get(id);
        }
        if (entity != null) {
            GroupInfo group = groupCache.get(entity.getGroupId());
            if (group != null) {
                ResourceStorage<? extends Info> storage = storages.get(group.getType());
                Map<String, String> pathCacheMap = storage.requirePath() ? pathCache.get(storage.folder()) : null;
                if (action == EventAction.DELETE) {
                    fileMappings.remove(id);
                    entity = fileCache.remove(id);
                    if (pathCacheMap != null) {
                        pathCacheMap.remove(id);
                    }
                } else {
                    Resource resource = fileMappings.get(id);
                    resource.readAll();
                    if (resource.exists()) {
                        entity = storage.read(resource.read());
                        putFile(storage, entity, resource);
                    } else {
                        this.readAll();
                        entity = fileCache.get(id);
                    }
                }
                publisher.publishEvent(new FileEvent(group.getType(), action, entity, Constants.EVENT_SOURCE_NOTIFY));
            }
        }
        return false;
    }

    @Override
    public void onApplicationEvent(ApplicationStartedEvent event) {
        read(false);
    }
}
